/*
 * MemoryStore.java Created on February 16, 2006, 10:38 AM
 */

package com.janrain.openid.store;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.janrain.openid.Association;
import com.janrain.openid.Util;

/**
 * This class is an in-memory store implementation. It is serializable to
 * support persistence across application restarts. It is thread-safe, to
 * support concurrent access from multiple threads.
 * 
 * @author JanRain, Inc.
 */
public class MemoryStore extends OpenIDStore implements Serializable
{
    static final long serialVersionUID = 8099032624276762250L;
    private static final String cls = "com.janrain.openid.store.MemoryStore";
    private static final Logger logger = Logger.getLogger(cls);

    private static final long NONCE_LIFE = 60 * 60 * 6; // six hours, in seconds

    private Map assocMap = Collections.synchronizedMap(new HashMap());
    private Map nonceMap = Collections.synchronizedMap(new HashMap());
    private byte [] authKey;

    /**
     * Creates a new <code>MemoryStore</code> with a random authKey.
     */
    public MemoryStore()
    {
        authKey = Util.randomBytes(AUTH_KEY_LEN);
    }

    /**
     * Creates a new <code>MemoryStore</code> with an authKey generated by
     * calculating the sha-1 hash of the secret phrase passed in.
     * 
     * @param secretPhrase
     *            a phrase to hash
     */
    public MemoryStore(String secretPhrase)
    {
        try
        {
            authKey = Util.sha1(secretPhrase.getBytes("UTF-8"));
        }
        catch (UnsupportedEncodingException e)
        {
            logger.log(Level.SEVERE, "UTF-8 again?", e);
        }
    }

    /**
     * Creates a new <code>MemoryStore</code> with an authKey passed in by the
     * user. The length of the key must be exactly
     * <code>com.janrain.openid.store.OpenIDStore.AUTH_KEY_LEN</code> bytes.
     * 
     * @param authKey
     *            the authKey to use
     */
    public MemoryStore(byte [] authKey)
    {
        if (authKey.length != AUTH_KEY_LEN)
        {
            throw new IllegalArgumentException("authKey not the correct length");
        }
        authKey = new byte[20];
        System.arraycopy(authKey, 0, this.authKey, 0, AUTH_KEY_LEN);
    }

    public boolean useNonce(String nonce)
    {
        Long ts = (Long)nonceMap.remove(nonce);

        return ts != null && Util.getTimeStamp() < ts.longValue() + NONCE_LIFE;
    }

    public void storeNonce(String nonce)
    {
        nonceMap.put(nonce, new Long(Util.getTimeStamp()));
    }

    public void storeAssociation(String serverUrl, Association assoc)
    {
        synchronized (assocMap)
        {
            List l = (List)assocMap.get(serverUrl);
            if (l == null)
            {
                l = new ArrayList();
                assocMap.put(serverUrl, l);
            }
            l.add(assoc);
        }
    }

    public boolean removeAssociation(String serverUrl, String handle)
    {
        boolean found = false;

        synchronized (assocMap)
        {
            List l = (List)assocMap.get(serverUrl);
            if (l != null)
            {
                Iterator it = l.iterator();
                while (it.hasNext())
                {
                    Association assoc = (Association)it.next();
                    if (assoc.getHandle().equals(handle))
                    {
                        it.remove();
                        found = true;
                    }
                }
            }
        }
        return found;
    }

    public Association getAssociation(String serverUrl, String handle)
    {
        synchronized (assocMap)
        {
            List l = (List)assocMap.get(serverUrl);

            Association result = null;

            if (l == null)
            {
                // nothing to do, result is already null
            }
            else if (handle == null)
            {
                Iterator it = l.iterator();
                while (it.hasNext())
                {
                    Association a = (Association)it.next();
                    if (a.getRemainingLife() <= 0)
                    {
                        it.remove();
                    }
                    else if (result == null
                            || a.getIssued() > result.getIssued())
                    {
                        result = a;
                    }
                }
            }
            else
            {
                Iterator it = l.iterator();
                while (it.hasNext())
                {
                    Association a = (Association)it.next();
                    if (a.getRemainingLife() <= 0)
                    {
                        it.remove();
                    }
                    else if (a.getHandle().equals(handle))
                    {
                        result = a;
                    }
                }
            }

            return result;
        }
    }

    public byte [] getAuthKey()
    {
        return authKey;
    }

    public void gc()
    {
        long now = Util.getTimeStamp();

        synchronized (nonceMap)
        {
            Iterator it = nonceMap.entrySet().iterator();
            while (it.hasNext())
            {
                Map.Entry e = (Map.Entry)it.next();
                Long l = (Long)e.getValue();
                if (l.longValue() + NONCE_LIFE < now)
                {
                    it.remove();
                }
            }
        }

        synchronized (assocMap)
        {
            Iterator it = assocMap.entrySet().iterator();
            while (it.hasNext())
            {
                Map.Entry e = (Map.Entry)it.next();
                Association a = (Association)e.getValue();
                if (a.getRemainingLife() <= 0)
                {
                    it.remove();
                }
            }
        }
    }
}
